package com.marketlive.system.encryption;

/*
(C) Copyright MarketLive. 2006. All rights reserved.
MarketLive is a trademark of MarketLive, Inc.
Warning: This computer program is protected by copyright law and international treaties.
Unauthorized reproduction or distribution of this program, or any portion of it, may result
in severe civil and criminal penalties, and will be prosecuted to the maximum extent
possible under the law.
*/

import java.io.File;
import java.io.FileInputStream;
import java.security.Key;
import java.security.PrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Properties;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.PBEParameterSpec;
//import javax.crypto.spec.SecretKeySpec;

//import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.marketlive.system.config.IConfigurationManager;
import org.marketlive.system.encryption.DecryptionException;
import org.marketlive.system.encryption.IDecryptor;
import org.marketlive.system.encryption.IKeyStore;
import org.marketlive.system.encryption.IKeyStoreManager;

//import com.marketlive.system.keymanagement.DBPropertiesBean;
import com.marketlive.system.keymanagement.KeystoreManager;
import com.marketlive.system.keymanagement.NonexistentKEKParameterException;

/**
 * Implementation of the Bouncy Castle (BC) RSA Encryption/Decryption algorithm.
 * Used to decrypt Credit Card Numbers and Passwords.
 * 
 */
public class BCRSADecryptor implements IDecryptor {
	
    private static Log log = LogFactory.getLog(BCRSADecryptor.class);
    private static final String ALGORITHM = "RSA";
    private static final String PROVIDER = "BC";
    private static final String KEK_EXTENSION = "kek";
    private static final String DES_ALGORITHM = "DESede";
    private static final String ERR_LOADING_KEK = "Error Loading KEK.";
    private static final String ERR_NO_KEK_FILE = "Cannot load Kek! File does not exist.";
    private static final String ERR_LOADING_PRIVATE_KEY = "Failed to load private key.";
    
    /** Added for PCI-DSS */
    private static final String PBE_ALGORITHM = "PBEWithSHA1AndDESede";

    /** HashMap to hold the public keys, referenced by key_code. */
    private HashMap privateKeyMap = new HashMap();
    
	/** Reference to the Configuration Manager. */
	private IConfigurationManager configurationManager;    

    /** Reference to the KeyStore Manager. */
    private IKeyStoreManager keyStoreManager;
    
    /** References to properties in new KEK file - for PCI-DSS 
     *  The keycode of the set of keys */
    private String keycode = null;
    
    /** The salt used to generate the PBE secret key - for PCI-DSS */
    private String salt = null;
    
    /** The number of iterations for the PBE algorithm - for PCI-DSS */
    private short iterations;
    
	/** Injects the Configuration Manager reference into this class. */
	public void setConfigurationManager(IConfigurationManager configurationManager) {
		this.configurationManager = configurationManager;
	}    

    /** Injects the KeyStore Manager reference into this class. */
    public void setKeyStoreManager(IKeyStoreManager keyStoreManager) {
        this.keyStoreManager = keyStoreManager;
    }

    /**
     * Init method to setup encryption/decryption.
     */
    public void init() {
    	if (log.isDebugEnabled()) {
    		log.debug("init called");
    	}    	
    }

    /**
     * Decrypt the input string using BC RSA encryption and return the decrypted string.
     * @param encryptedString the encrypted credit string to decrypt.
     * @return returns an unencrypted string.
     */
    public String decrypt(String encryptedString) throws DecryptionException {
        String keyCode = keyStoreManager.getKeyCode();
        String decryptedString = decrypt(keyCode, encryptedString);
        return decryptedString;
    }

    /**
     * Decrypt the encrypted input string using BC RSA encryption and return the decrypted string.
     * @param keyCode the keyCode to lookup the private key from the KeyStore table.
     * @param encryptedString the encrypted string to decipher.
     * @return returns an unencrypted string.
     */
    public String decrypt(String keyCode, String encryptedString) throws DecryptionException {
        RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyMap.get(keyCode);
        if (privateKey == null) {
            IKeyStore keyStore = keyStoreManager.findByCode(keyCode);
            if (keyStore == null) {
                throw new RuntimeException("Error loading keystore from BCRSA Decryptor.");
            }
            try {
            	String privateKeyRep = keyStore.getPrivateKey();
            	privateKey = (RSAPrivateKey) getPrivateKey(privateKeyRep);
            } catch (Exception e) {
                throw new DecryptionException("Error reading private key. Verify that your version of Bouncy Castle matches the version used for key creation.", e);                
            }
            privateKeyMap.put(keyCode, privateKey);
        }
        String decryptedString = "";
        try {
            // Base-64 decode the string, then RSA Decrypt the string.
        	byte [] decodedStringArray = keyStoreManager.decodeBase64(encryptedString);
            decryptedString = decryptRSA(decodedStringArray, privateKey);
        } catch (Exception e) {
            throw new DecryptionException("Error with base64 decoding in BCRSA Decryptor.", e);
        }
        return decryptedString;
    }

    /**
     * Decrypts a string using the implemented decryption scheme and returns decrypted string.
     * @param encryptedData the string to decrypt.
     * @return returns a decryptedString.
     */
    private final String decryptRSA(byte[] encryptedData, RSAPrivateKey privateKey) throws DecryptionException {
        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", PROVIDER);
            cipher.init(Cipher.DECRYPT_MODE,privateKey);
            String decryptedString = new String(cipher.doFinal(encryptedData), "UTF-8");
            return decryptedString;
        } catch (Exception e) {
            throw new DecryptionException("Error Decrypting String", e);
        }
    }
    
    /**
     * Wrapper method that loads a kek from file and unwraps a private key from file.
     */
    public PrivateKey getPrivateKey(String inString) {
    	
    	if (null == inString) {
    		log.error("Null private key string" + inString);
    		return null;
    	}
    	
       	PrivateKey privKey = null;
    	Key kek = null;
    	try {   	
	    	kek = loadKek(keyStoreManager.getKeyCode() + "." + KEK_EXTENSION);
            if (null == kek) {
                log.error("Null KEK received");
                return null;
            }
	    	privKey = unwrapKey(kek, inString);
    	} catch (Exception e) {
    			log.error("Caught exception retrieving private key", e);
    	}
    	return privKey;
    }
    
    /**
     * Load KEK.
     * 
     * This routine was modified to accomodate the new PBE algorithm for
     * generating the KEK.  PCI-DSS requires that plaintext crypto keys 
     * are not accessible on the file-system.
     * 
     * @param path The path to the file containing the key.
     * @return a key object.
     */
    private Key loadKek(String path) {
        
//      Modified for PCI-DSS - not needed anymore
//    	Key tripleDesKey = null;
//        String kekString = null;
//        byte [] kekArray =  null;
        
        String rootDir = configurationManager.getAsString("system.io.path.encryption.key");
        String propertyPath = rootDir +  path;
        
        // Added for PCI-DSS
        Properties kekprops = new Properties();
        SecretKey pbekey = null;
        
        try {
            File file = new File(propertyPath);
            //logger.debug("KEK file:" + path);
            //System.out.println("KEK file:" + propertyPath);
            if(!file.exists()) {
            	log.error(ERR_NO_KEK_FILE + ": " + propertyPath);
                throw new RuntimeException(ERR_NO_KEK_FILE + ": " + propertyPath);
            }
            
            // Modified for PCI-DSS - not needed anymore
//            BufferedReader br = new BufferedReader(new FileReader(propertyPath));
//            kekString = br.readLine();
//            kekArray =  keyStoreManager.decodeBase64(kekString);
//            tripleDesKey = new SecretKeySpec(kekArray, DES_ALGORITHM);                        
//            br.close();
            

            // Added for PCI-DSS new PBE KEK
            
            // Load and check KEK properties
            kekprops.load(new FileInputStream(file));
        
            // Check if keycode property is NOT in there
            if (!kekprops.containsKey("keycode"))
                throw new NonexistentKEKParameterException("KEK properties does not contain the \"keycode\" property in it");
            
            keycode = kekprops.getProperty("keycode");
            
            // Check if salt property is NOT in there
            if (!kekprops.containsKey("salt"))
                throw new NonexistentKEKParameterException("KEK properties does not contain the \"salt\" property in it");
            
            salt = kekprops.getProperty("salt");
            
            // Check if iterations property is NOT in there
            if (!kekprops.containsKey("iterations"))
                throw new NonexistentKEKParameterException("KEK properties does not contain the \"iterations\" property in it");
            
            iterations = Short.parseShort(kekprops.getProperty("iterations"));
            
            // Generate the PBE key and encrypt some data for testing
            pbekey = new KeystoreManager().generatePBEKey(keycode);
                
                
        } catch (Exception e) {
        	log.error(ERR_LOADING_KEK, e);
            throw new RuntimeException(ERR_LOADING_KEK, e);
        }
        return pbekey;
    }    
    
    /**
     * Unwrap a key that was previously wrapped and saved to a file.
     * @param kekPath The absolute path to the file where the KEK is stored.
     * @param encryptedKeyPath The absolute path to the file where the wrapped key is stored.
     * @return A private key.
     */
    private PrivateKey unwrapKey(Key kek, String inString) {
        RSAPrivateKey rsaPrivateKey = null;
        byte [] aByte =  null;
        if (null == kek) {
        	return null;
        }        
        if (null == inString) {
        	return null;
        }
        try {
            aByte =  loadEncryptedKey(inString);
            if (aByte != null) {
	            Cipher cipher = Cipher.getInstance(PBE_ALGORITHM);
                // Modified for PCI-DSS
//	            cipher.init(Cipher.UNWRAP_MODE, kek, cipher.getParameters());
                cipher.init(Cipher.UNWRAP_MODE, kek,
                        new PBEParameterSpec(salt.getBytes("UTF-8"), iterations));                
	            rsaPrivateKey = (RSAPrivateKey)cipher.unwrap(aByte, ALGORITHM, Cipher.PRIVATE_KEY);
            } else {
            	log.error(ERR_LOADING_PRIVATE_KEY);
            	System.out.println(ERR_LOADING_PRIVATE_KEY);
            }
        } catch(Exception exception) {
            log.error("unwrapKey Exception:" + exception);
        }
        return rsaPrivateKey;
    }

    /**
     * Load data from a previously base64 encoded key into a byte array.
     * @param path The absolute path to the file where the wrapped key is stored.
     * @return A byte array of the wrapped key.
     */
    private byte [] loadEncryptedKey(String inString) {
    	
        byte [] aByte = null;
        
        try {
            aByte =  keyStoreManager.decodeBase64(inString);
        } catch (Exception e) {
            log.error("loadEncryptedKey Exception:" + e);
            System.out.println("loadEncryptedKey Exception:" + e);
        }
        return aByte;
    }     
    
    /**
     * Compares two encrypted strings and returns true if they are equal.  Requires decryption to do this.
     * @param encryptedString1  the first encrypted string to compare.
     * @param encryptedString2 the second encrypted string to compare.
     * @return returns true if the two values decrypt to the same value.
     */
    public boolean compare(String encryptedString1, String encryptedString2) throws DecryptionException {
        if (encryptedString1 == null || encryptedString2 == null) {
            return false;
        }
        String value1 = decrypt(encryptedString1);
        String value2 = decrypt(encryptedString2);
        if (value1.equals(value2)) {
            return true;   
        }
        return false;
    }    
    
}
